Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

36장. Service와 의존성

Service는 흐름을 조율하는 객체라고 했습니다.
그리고 Service는 Repository에 의존합니다.

여기서 “의존“이라는 말이 나옵니다.
이 장에서는 의존성을 어떻게 다루는지,
그리고 그 유명한 “의존성 주입“이 무엇인지 배웁니다.

11부의 마무리이자,
스프링을 이해하는 데 가장 중요한 개념입니다.


36.1 Service의 역할

Service는 하나의 기능을
처음부터 끝까지 완성합니다.

예를 들어 주문 생성은 이런 흐름입니다.

  • 회원이 존재하는지 확인
  • 상품이 존재하는지 확인
  • 주문 객체를 만든다
  • 주문을 저장한다
  • 응답을 돌려준다

이 흐름을 Service가 조율합니다.

class OrderService(
    private val orderRepository: OrderRepository,
    private val productRepository: ProductRepository
) {
    fun createOrder(request: CreateOrderRequest): OrderResponse {
        val product = productRepository.findById(request.productId)
            ?: throw IllegalArgumentException("상품 없음")

        val order = Order(id = 0, items = listOf(OrderItem(product.id, request.quantity)))
        val saved = orderRepository.save(order)
        return saved.toResponse()
    }
}

Service는 여러 객체를 엮어
“줄거리“를 완성하는 감독 같은 존재입니다.


36.2 생성자를 통한 의존성 전달

OrderService
Repository들이 있어야 동작합니다.

이 필요한 객체들을
생성자로 받는 것이 핵심입니다.

class OrderService(
    private val orderRepository: OrderRepository,
    private val productRepository: ProductRepository
)

Service가 필요한 것을
스스로 만들지 않고 “밖에서 받는다“는 점이 중요합니다.

// 나쁜 예: Service가 직접 만든다
class OrderService {
    private val orderRepository = MemoryOrderRepository()  // 특정 구현에 묶임
}

직접 만들면
그 특정 구현에 딱 붙어 버립니다.
그러면 테스트에서 교체할 수 없습니다.


36.3 구현체가 아닌 Interface에 의존하기

생성자로 받되,
받는 타입은 구현체가 아니라 인터페이스여야 합니다. (8장)

class OrderService(
    private val orderRepository: OrderRepository   // 인터페이스!
)

OrderRepository는 인터페이스이므로
어떤 구현체든 넣을 수 있습니다.

  • 운영에서는 데이터베이스 구현
  • 테스트에서는 메모리/Fake 구현

이것이 31장에서 테스트가
그토록 쉬웠던 이유입니다.

구현이 아닌 인터페이스에 의존하라.
이 한 문장이 좋은 구조의 핵심입니다.


36.4 Dependency Injection의 기본 개념

지금까지 한 일에 이름이 있습니다.
바로 의존성 주입(Dependency Injection, DI)입니다.

말은 거창하지만 뜻은 간단합니다.

필요한 객체를 밖에서 만들어 넣어 주는 것.

여기서는 프레임워크 없이
직접 객체를 조립합니다.

OrderService
 ├─ OrderRepository
 └─ ProductRepository

조립 코드는 이렇게 생깁니다.

fun main() {
    // 구현체를 만들고
    val orderRepository = MemoryOrderRepository()
    val productRepository = MemoryProductRepository()

    // Service에 넣어 준다 (주입)
    val orderService = OrderService(orderRepository, productRepository)

    // 이제 사용
    orderService.createOrder(request)
}

이렇게 객체를 만들어 연결하는 코드를
조립(assembly)이라고 부릅니다.

스프링은 바로 이 조립을
자동으로 해 주는 도구입니다.
지금 우리는 그것을 손으로 해 본 것입니다.


36.5 Service 테스트하기

의존성을 생성자로 받으면
테스트가 아주 쉬워집니다. (30·31장)

@Test
fun `상품이 없으면 주문 생성에 실패한다`() {
    // Given: 빈 Fake 저장소
    val orderService = OrderService(
        FakeOrderRepository(),
        FakeProductRepository()   // 상품이 하나도 없음
    )

    // When & Then
    assertThrows<IllegalArgumentException> {
        orderService.createOrder(CreateOrderRequest(productId = 999, quantity = 1))
    }
}

진짜 데이터베이스 없이
원하는 상황을 자유롭게 만들 수 있습니다.


36.6 Fake Repository 활용하기

성공 시나리오도 테스트해 봅시다.

@Test
fun `주문을 생성하면 저장된다`() {
    // Given
    val productRepository = FakeProductRepository()
    productRepository.save(Product(id = 1, name = "노트북", price = 1000))

    val orderRepository = FakeOrderRepository()
    val orderService = OrderService(orderRepository, productRepository)

    // When
    val response = orderService.createOrder(
        CreateOrderRequest(productId = 1, quantity = 2)
    )

    // Then
    assertNotNull(orderRepository.findById(response.id))
}

Fake를 미리 채워 두고
원하는 상태에서 검증합니다.

이렇게 빠르고 안정적인 테스트가
좋은 구조에서 자연스럽게 나옵니다.


36.7 의존성이 늘어날 때 발생하는 문제

서비스가 커지면
의존성도 함께 늘어납니다.

class OrderService(
    private val orderRepository: OrderRepository,
    private val productRepository: ProductRepository,
    private val userRepository: UserRepository,
    private val paymentService: PaymentService,
    private val notificationService: NotificationService
    // ... 계속 늘어남
)

손으로 조립하는 코드도 점점 길어집니다.

val orderService = OrderService(
    orderRepository,
    productRepository,
    userRepository,
    paymentService,
    notificationService
)

객체가 수십, 수백 개가 되면
이 조립을 손으로 관리하기 벅차집니다.

바로 이 문제를 해결하려고
스프링 같은 DI 프레임워크가 등장합니다.

프레임워크는 이 “조립“을 대신해 줍니다.
우리가 손으로 한 일을 자동화한 것뿐입니다.

그래서 지금 이 과정을 직접 해 본 것이
나중에 스프링을 배울 때 큰 도움이 됩니다.


36장을 마치며

이 장에서 우리는 다음을 배웠습니다.

  • Service가 흐름을 조율하는 역할이라는 점
  • 의존성을 생성자로 받는 이유
  • 구현체가 아닌 인터페이스에 의존하기
  • 의존성 주입(DI)과 조립의 개념
  • Fake로 Service를 쉽게 테스트하기
  • 의존성이 늘면 프레임워크가 필요해지는 이유

이것으로 11부(백엔드 구조)를 마칩니다.
다음 12부에서는 지금까지 배운 모든 것을 모아
하나의 주문 시스템을 완성합니다.